Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 

247 lignes
11 KiB

  1. <template>
  2. <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 lg:py-10">
  3. <nav aria-label="面包屑" class="flex items-center gap-1.5 text-xs text-[var(--c-8a97a8)]">
  4. <NuxtLink to="/" class="hover:text-[var(--c-0b8cff)]">首页</NuxtLink>
  5. <span>/</span>
  6. <NuxtLink to="/pic/" class="hover:text-[var(--c-0b8cff)]">AI 图片</NuxtLink>
  7. <span>/</span>
  8. <span class="line-clamp-1 text-[var(--c-4b5b70)]">{{ pageTitle }}</span>
  9. </nav>
  10. <div class="mt-5 lg:grid lg:grid-cols-12 lg:gap-8">
  11. <!-- 主图 -->
  12. <div class="lg:col-span-8">
  13. <div class="overflow-hidden rounded-2xl bg-[var(--c-ffffff)] p-4 shadow-[0_8px_28px_var(--sh-34-68-112-50)] sm:p-6">
  14. <div class="overflow-hidden rounded-xl bg-[var(--c-eef3f8)]">
  15. <img
  16. v-if="currentImg"
  17. :src="currentImg"
  18. :alt="pageTitle"
  19. class="mx-auto max-h-[70vh] w-auto object-contain"
  20. >
  21. </div>
  22. <!-- 多图切换 -->
  23. <div v-if="picList.length > 1" class="mt-4 flex flex-wrap gap-2">
  24. <button
  25. v-for="(img, idx) in picList"
  26. :key="idx"
  27. type="button"
  28. class="size-16 overflow-hidden rounded-lg border-2 transition-colors"
  29. :class="idx === imgIndex ? 'border-[var(--c-0b8cff)]' : 'border-transparent hover:border-[var(--c-e5edf7)]'"
  30. @click="imgIndex = idx"
  31. >
  32. <img :src="img" class="size-full object-cover" :alt="`图 ${idx + 1}`" loading="lazy">
  33. </button>
  34. </div>
  35. <!-- 信息 -->
  36. <div class="mt-6 border-t border-[var(--c-eef2f7)] pt-5">
  37. <h1 class="text-lg font-extrabold leading-snug text-[var(--c-172033)] sm:text-xl">{{ pageTitle }}</h1>
  38. <div class="mt-3 flex flex-wrap items-center gap-3 text-xs text-[var(--c-a0aabb)]">
  39. <span class="rounded-full bg-[var(--c-f3f0ff)] px-2 py-0.5 font-bold text-[var(--c-7a5af8)]">{{ aiTypeName }}</span>
  40. <span v-if="detail.author_name" class="inline-flex items-center gap-1">
  41. <UIcon name="i-lucide-user-round" class="size-3.5" />
  42. {{ detail.author_name }}
  43. </span>
  44. <span class="inline-flex items-center gap-1">
  45. <UIcon name="i-lucide-calendar" class="size-3.5" />
  46. {{ formatTime(detail.add_time) }}
  47. </span>
  48. <span v-if="detail.view_num" class="inline-flex items-center gap-1">
  49. <UIcon name="i-lucide-eye" class="size-3.5" />
  50. {{ detail.view_num }}
  51. </span>
  52. <span v-if="detail.like_num" class="inline-flex items-center gap-1">
  53. <UIcon name="i-lucide-heart" class="size-3.5" />
  54. {{ detail.like_num }}
  55. </span>
  56. <span v-if="detail.resolution" class="inline-flex items-center gap-1">
  57. <UIcon name="i-lucide-ratio" class="size-3.5" />
  58. {{ detail.resolution }}
  59. </span>
  60. </div>
  61. <!-- 转化 -->
  62. <div class="mt-6 flex flex-wrap gap-3">
  63. <NuxtLink
  64. to="/draw/anime/"
  65. class="inline-flex h-11 items-center gap-1.5 rounded-full bg-gradient-to-r from-[var(--c-0b8cff)] to-[var(--c-3aa9ff)] px-6 text-sm font-bold text-white shadow-[0_10px_22px_var(--sh-11-140-255-240)] transition-opacity hover:opacity-90"
  66. >
  67. <UIcon name="i-lucide-zap" class="size-4" />
  68. 我也要生成
  69. </NuxtLink>
  70. <a
  71. v-if="currentImg"
  72. :href="currentImg"
  73. target="_blank"
  74. rel="noopener"
  75. class="inline-flex h-11 items-center gap-1.5 rounded-full bg-[var(--c-eef7ff)] px-6 text-sm font-bold text-[var(--c-0b8cff)] transition-colors hover:bg-[var(--c-e0f0ff)]"
  76. >
  77. <UIcon name="i-lucide-external-link" class="size-4" />
  78. 查看原图
  79. </a>
  80. </div>
  81. <!-- 上一件 / 下一件(同时利于站内链接与收录) -->
  82. <div
  83. v-if="detail.prev_chat_open_id || detail.next_chat_open_id"
  84. class="mt-6 grid gap-3 border-t border-[var(--c-eef2f7)] pt-5 sm:grid-cols-2"
  85. >
  86. <NuxtLink
  87. v-if="detail.prev_chat_open_id"
  88. :to="`/pic/${detail.prev_chat_open_id}_0.html`"
  89. rel="prev"
  90. class="group flex items-center gap-2 rounded-xl bg-[var(--c-f7f9fd)] p-4 transition-colors hover:bg-[var(--c-eef7ff)]"
  91. >
  92. <UIcon name="i-lucide-chevron-left" class="size-4 shrink-0 text-[var(--c-a0aabb)]" />
  93. <div class="min-w-0">
  94. <div class="text-xs text-[var(--c-a0aabb)]">上一件作品</div>
  95. <div class="mt-0.5 text-sm font-bold text-[var(--c-4b5b70)] transition-colors group-hover:text-[var(--c-0b8cff)]">查看</div>
  96. </div>
  97. </NuxtLink>
  98. <NuxtLink
  99. v-if="detail.next_chat_open_id"
  100. :to="`/pic/${detail.next_chat_open_id}_0.html`"
  101. rel="next"
  102. class="group flex items-center justify-end gap-2 rounded-xl bg-[var(--c-f7f9fd)] p-4 text-right transition-colors hover:bg-[var(--c-eef7ff)] sm:col-start-2"
  103. >
  104. <div class="min-w-0">
  105. <div class="text-xs text-[var(--c-a0aabb)]">下一件作品</div>
  106. <div class="mt-0.5 text-sm font-bold text-[var(--c-4b5b70)] transition-colors group-hover:text-[var(--c-0b8cff)]">查看</div>
  107. </div>
  108. <UIcon name="i-lucide-chevron-right" class="size-4 shrink-0 text-[var(--c-a0aabb)]" />
  109. </NuxtLink>
  110. </div>
  111. </div>
  112. </div>
  113. </div>
  114. <!-- 相关作品 -->
  115. <div class="mt-8 lg:col-span-4 lg:mt-0">
  116. <div class="rounded-2xl bg-[var(--c-ffffff)] p-5 shadow-[0_8px_28px_var(--sh-34-68-112-50)]">
  117. <h2 class="text-base font-extrabold text-[var(--c-172033)]">更多 AI 作品</h2>
  118. <div v-if="otherList.length" class="mt-4 grid grid-cols-3 gap-2">
  119. <NuxtLink
  120. v-for="item in otherList"
  121. :key="`${item.chat_open_id}_${item.img_index}`"
  122. :to="`/pic/${item.chat_open_id}_${item.img_index || 0}.html`"
  123. class="group aspect-square overflow-hidden rounded-lg bg-[var(--c-eef3f8)]"
  124. >
  125. <img
  126. :src="item.img"
  127. :alt="item.prompts || 'AI 作品'"
  128. class="size-full object-cover transition-transform duration-300 group-hover:scale-110"
  129. loading="lazy"
  130. >
  131. </NuxtLink>
  132. </div>
  133. <div v-else class="py-8 text-center text-sm text-[var(--c-a0aabb)]">暂无更多作品</div>
  134. <NuxtLink
  135. to="/pic/"
  136. class="mt-4 flex items-center justify-center gap-1 text-sm font-bold text-[var(--c-0b8cff)] hover:underline"
  137. >
  138. 查看全部图片
  139. <UIcon name="i-lucide-arrow-right" class="size-4" />
  140. </NuxtLink>
  141. </div>
  142. </div>
  143. </div>
  144. </div>
  145. </template>
  146. <script setup>
  147. import dayjs from 'dayjs'
  148. import { legacyPageTitle } from '~/composables/useLegacySeo'
  149. // ai_type:1 gpt 2 MJ绘画 3/7/8 AI画头像 4 变年龄 5 变性别 6 AI绘画(沿用原站映射)
  150. const AI_TYPE_MAP = {
  151. 1: 'AI 问答', 2: 'MJ 绘画', 3: 'AI 画头像', 4: 'AI 变年龄',
  152. 5: 'AI 变性别', 6: 'AI 绘画', 7: 'AI 画头像', 8: 'AI 画头像',
  153. }
  154. const SEO_AI_TYPE_MAP = {
  155. 1: 'gpt', 2: 'MJ绘画', 3: 'AI画头像', 4: 'AI变年龄',
  156. 5: 'AI变性别', 6: 'AI绘画', 7: 'AI画头像', 8: 'AI画头像',
  157. }
  158. const route = useRoute()
  159. const { post } = useApi()
  160. // 原站 URL 形式:/pic/{chat_open_id}_{img_index}.html
  161. const raw = String(route.params.id || '').replace(/\.html$/i, '')
  162. const chatOpenId = raw.split('_')[0]
  163. const initialIndex = Number(raw.split('_')[1] || 0)
  164. const { data, error } = await useAsyncData(`pic-${raw}`, async () => {
  165. const detailRes = await post('/art/getdetail', { chat_open_id: chatOpenId, prev_next: 1 })
  166. const d = detailRes.data || {}
  167. const otherRes = await post('/art/getlist', { page_no: 1, page_size: 12, curr_id: d.id })
  168. .catch(() => ({ list: [] }))
  169. return { detail: d, otherList: (otherRes.list || []).filter(i => i.img).slice(0, 9) }
  170. })
  171. if (error.value || !data.value?.detail) {
  172. throw createError({ statusCode: 404, statusMessage: '作品不存在', fatal: true })
  173. }
  174. const detail = computed(() => data.value.detail || {})
  175. const otherList = computed(() => data.value.otherList || [])
  176. const picList = computed(() => (detail.value.content ? String(detail.value.content).split(',') : []))
  177. const imgIndex = ref(initialIndex)
  178. const currentImg = computed(() => picList.value[imgIndex.value] || picList.value[0] || '')
  179. const aiTypeName = computed(() => AI_TYPE_MAP[Number(detail.value.ai_type)] || 'AI 作品')
  180. const seoAiTypeName = computed(() => SEO_AI_TYPE_MAP[Number(detail.value.ai_type)] || 'AI作品')
  181. function formatTime(v) {
  182. return v ? dayjs(Number(v) * 1000).format('YYYY-MM-DD HH:mm') : ''
  183. }
  184. // 页面内展示标题保持现有样式。
  185. const pageTitle = computed(() => {
  186. const p = String(detail.value.prompts || '').trim()
  187. if (p) return p
  188. const day = detail.value.add_time ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD') : ''
  189. return `AI会员生成的${aiTypeName.value}(${day})`
  190. })
  191. // TDK 严格沿用旧 PC 站 pic/detail controller 的生成规则。
  192. const seoPageTitle = computed(() => {
  193. const prompts = String(detail.value.prompts || '').trim()
  194. if (prompts) return prompts
  195. const day = detail.value.add_time
  196. ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD')
  197. : ''
  198. return `AI会员(${detail.value.author_open_id})生成的${seoAiTypeName.value}(${day}00${picList.value.length})`
  199. })
  200. const seoDescription = computed(() => {
  201. const day = detail.value.add_time
  202. ? dayjs(Number(detail.value.add_time) * 1000).format('YYYYMMDD')
  203. : ''
  204. return `由会员(${detail.value.author_open_id})(${day})生成的${seoAiTypeName.value}`
  205. })
  206. useSeoMeta({
  207. title: () => legacyPageTitle(seoPageTitle.value),
  208. description: seoDescription,
  209. keywords: 'AI画头像',
  210. ogTitle: seoPageTitle,
  211. ogType: 'article',
  212. ogImage: currentImg,
  213. })
  214. useHead({
  215. link: [{ rel: 'canonical', href: `https://tool.aionline.cc/pic/${raw}.html` }],
  216. script: [{
  217. type: 'application/ld+json',
  218. innerHTML: computed(() => JSON.stringify({
  219. '@context': 'https://schema.org',
  220. '@type': 'ImageObject',
  221. name: pageTitle.value,
  222. contentUrl: currentImg.value,
  223. description: detail.value.prompts || aiTypeName.value,
  224. creator: { '@type': 'Organization', name: 'AI在线' },
  225. })),
  226. }],
  227. })
  228. </script>